﻿using LittleBee.IDAL;
using LittleBee.JsonRpc;
using LittleBee.Utils;
using Microsoft.CSharp;
using Newtonsoft.Json.Linq;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Reflection;
using System.Text;

namespace LittleBee.Cells
{
	public class CellService : IDisposable
	{
		private static readonly string _strCellDllPath = Environment.CurrentDirectory;

		public Dictionary<Int64, string> GetCellTypeList()
		{
			Dictionary<Int64, string> dctCellType = new Dictionary<Int64, string>();

			if (_dba.QueryCellTypeList(out DataSet? dsCellInfos) == true)
			{
				foreach (DataRow item in dsCellInfos!.Tables[0].Rows)
				{
					dctCellType.Add(item.Field<Int64>("cell_id"), item.Field<string>("type")!);
				}
			}

			return dctCellType;
		}

		/// <summary>
		/// get all cell app list, and return them grouped with cell id
		/// {{cell_id_1:{{app_id, app_name}, ...}, cell_id_2:{{app_id, app_name}, ...}}
		/// </summary>
		/// <returns>cell app list</returns>
		public Dictionary<Int64, Dictionary<Int64, string>> GetCellAppList()
		{
			var dctCellID = new Dictionary<Int64, Dictionary<Int64, string>>();

			if (_dba.QueryCellAppList(out DataSet? dsAppInfos) == true)
			{
				foreach (DataRow item in dsAppInfos!.Tables[0].Rows)
				{
					Int64 nCellID = item.Field<Int64>("cell_id");
					Int64 nAppID = item.Field<Int64>("app_id");
					string appName = item.Field<string>("app_name")!;

					if (dctCellID.TryGetValue(nCellID, out Dictionary<Int64, string>? dctAppID))
					{
						dctAppID[nAppID] = appName;
					}
					else
					{
						dctAppID = new Dictionary<Int64, string>();
						dctAppID[nAppID] = appName;
						dctCellID[nCellID] = dctAppID;
					}
				}
			}

			return dctCellID;
		}

		public Dictionary<Int64, string> GetCellAppList(string strCellType, out Int64 nCellID)
		{
			var dctAppID = new Dictionary<Int64, string>();
			nCellID = 0;

			if (_dba.QueryCellAppList(strCellType, out DataSet? dsAppInfos) == true)
			{
				foreach (DataRow item in dsAppInfos!.Tables[0].Rows)
				{
					dctAppID[item.Field<Int64>("app_id")] = item.Field<string>("app_name")!;
					nCellID = item.Field<Int64>("cell_id");
				}
			}

			return dctAppID;
		}

		public Dictionary<Int64, string> GetCellAppList(Int64 nCellID)
		{
			var dctAppID = new Dictionary<Int64, string>();

			if (_dba.QueryCellAppList(nCellID, out DataSet? dsAppInfos) == true)
			{
				foreach (DataRow item in dsAppInfos!.Tables[0].Rows)
				{
					dctAppID[item.Field<Int64>("app_id")] = item.Field<string>("app_name")!;
				}
			}

			return dctAppID;
		}

		internal void QueryAppPointers(Int64 instanceID, out DataTable? dtPointers)
		{
			_dba.QueryAppPointers(instanceID, out dtPointers);
		}

		private static readonly CellService _oSingleton = new CellService();

		public void SetCellMonitoringState(Int64 appid, bool blMonitoring)
		{
			if (_dctAppID2App.TryGetValue(appid, out Cell? cell) == true)
			{
				cell.MonitoringState = blMonitoring;
			}
			else
			{
				Console.WriteLine($"Not found appid: {appid}");
			}
		}

		public void SetPointerMonitoringState(Int64 appid, string pid, bool blMonitoring)
		{
			if (_dctAppID2App.TryGetValue(appid, out Cell? cell) == true)
			{
				try
				{
					cell[pid].MonitoringState = blMonitoring;
				}
				catch (Exception ex)
				{
					Console.WriteLine($"Not found Pointer: {appid}:{pid}: {ex.Message}");
				}
			}
			else
			{
				Console.WriteLine($"Not found appid: {appid}");
			}
		}

		private readonly LBAccess _dba = LBAccess.Instance;

		private readonly Dictionary<Int64, CellTypeInfo> _dctCellID2Cell = new Dictionary<Int64, CellTypeInfo>();

		/// <summary>
		/// 
		/// </summary>
		/// <param name="strCellType">Cell Type</param>
		/// <returns>Cell ID</returns>
		public Int64 GetCellID(string strCellType)
		{
			if (_dctCellType2Cell.TryGetValue(strCellType, out CellTypeInfo? info))
			{
				return info.ID;
			}
			else
			{
				throw new KeyNotFoundException($"Not found Cell Type: {strCellType}");
			}
		}

		private readonly Dictionary<string, CellTypeInfo> _dctCellType2Cell= new Dictionary<string, CellTypeInfo>();

		private readonly Dictionary<Int64, Cell> _dctAppID2App = new Dictionary<Int64, Cell>();
		private readonly Dictionary<Int64, List<Cell>> _dctCellID2Apps = new Dictionary<Int64, List<Cell>>();

		private readonly List<Int64> _lstLockedCellIDs = new List<Int64>();

		private readonly Dictionary<Int64, Dictionary<string, JObject>> _dctAppID2PointerPropertyChanged = new Dictionary<Int64, Dictionary<string, JObject>>();

		public event PointerUpdatedEventHandler? PointerUpdated;

		public static CellService Instance
		{
			get
			{
				return _oSingleton;
			}
		}

		public void Initialize()
		{
			LoadCellTypes();
			LoadApps();
		}

		internal void RemoveFirstLoaded(Int64 instanceID)
		{
			_dba.RemoveFirstLoaded(instanceID);
		}

		internal bool IsFirstLoaded(Int64 instanceID)
		{
			return _dba.IsFirstLoaded(instanceID);
		}

		private void LoadApps()
		{
			if (_dba.QueryCellAppsInfo(out DataSet? dsAppsInfo))
			{
				foreach (DataRow item in dsAppsInfo!.Tables[0].Rows)
				{
					Int64 app_id = item.Field<Int64>("app_id");
					Int64 cell_id = item.Field<Int64>("cell_id");

					try
					{
						LoadCellApp(cell_id, app_id);
					}
					catch (Exception ex)
					{
						Console.WriteLine($"Exception Occurred: {ex.Message}\n{ex.StackTrace}");
					}
				}
			}
		}

		private void LoadCellTypes()
		{
			if (_dba.QueryCellTypeList(out DataSet? dsCellInfos))
			{
				foreach (DataRow item in dsCellInfos!.Tables[0].Rows)
				{
					Int64 cell_id = item.Field<Int64>("cell_id");
					string strCellType = item.Field<string>("type")!;
					string strCode = item.Field<string>("code")!;
					string strCodeOrLibrary = item.Field<string>("code_or_library")!;
					string strLibrary = item.Field<string>("library")!;
					Type cell_type = GetCellConstructObject(cell_id, strCellType, strCodeOrLibrary, strCode, strLibrary);

					try
					{
						CellTypeInfo ci = new CellTypeInfo(item, cell_type);

						lock (_dctCellType2Cell)
						{
							lock (_dctCellID2Cell)
							{
								if (!_lstLockedCellIDs.Contains(cell_id))
								{
									_dctCellType2Cell[strCellType] = ci;
									_dctCellID2Cell[cell_id] = ci;
								}
								else
								{
									throw new CellTypeLockedException($"CellType {strCellType} is Locked.");
								}
							}
						}
					}
					catch (Exception ex)
					{
						Console.WriteLine($"Exception Occurred: {ex.Message}\n\n{ex.StackTrace}");
					}
				}
			}
		}

		public void LockCellType(Int64 cell_id)
		{
			lock (_dctCellType2Cell)
			{
				lock (_dctCellID2Cell)
				{
					_lstLockedCellIDs.Add(cell_id);
				}
			}
		}

		public void UnlockCellType(Int64 cell_id)
		{
			lock (_dctCellType2Cell)
			{
				lock (_dctCellID2Cell)
				{
					_lstLockedCellIDs.Remove(cell_id);
				}
			}
		}

		public void InstallCellType(out Int64 cell_id, JObject joRequest)
		{
			cell_id = 0;

			if (joRequest["cell_info"] is JObject joCellInfo)
			{
				var dctCellInfo = new Dictionary<string, object>()
				{
					{ "cell_uuid", joCellInfo["cell_uuid"]!.ToString() },
					{ "type", joCellInfo["type"]!.ToString() },
					{ "name", joCellInfo["name"]!.ToString() },
					{ "description", joCellInfo["description"]!.ToString() },
					{ "class", joCellInfo["class"]!.ToString() },
					{ "attributes", joCellInfo["attributes"]!.ToString() },
					{ "content", joCellInfo["content"]!.ToString() },
					{ "categories", joCellInfo["categories"]!.ToString() },
					{ "helper", joCellInfo["helper"]!.ToString() },
					{ "revisions", joCellInfo["revisions"]!.ToString() },
					{ "enable", true },
					{ "comm_set", joCellInfo["comm_set"]!.ToString() },
					{ "transfer_set", joCellInfo["transfer_set"]!.ToString() },
					{ "groups", joCellInfo["groups"]!.ToString() },
					{ "group_set", joCellInfo["group_set"]!.ToString() },
					{ "conditions", joCellInfo["conditions"]!.ToString() },
					{ "condition_set", joCellInfo["condition_set"]!.ToString() },
					{ "structs", joCellInfo["structs"]!.ToString() },
					{ "struct_set", joCellInfo["struct_set"]!.ToString() },
					{ "io_pointers", joCellInfo["io_pointers"]!.ToString() },
					{ "io_set", joCellInfo["io_set"]!.ToString() },
					{ "code_or_library", joCellInfo["code_or_library"]!.ToString() },
					{ "code", joCellInfo["code"]!.ToString() },
					{ "library", joCellInfo["library"]!.ToString() },
					{ "cell_id",  cell_id},
				};
				_dba.InsertCellInfo(out cell_id, dctCellInfo);

				if (joRequest["pointers"] is JArray jaCellPointers)
				{
					foreach (JObject item in jaCellPointers)
					{
						var dctCellPointer = new Dictionary<string, object>()
						{
							{"cell_id", cell_id },
							{"pid", item["pid"]!.ToString() },
							{"name", item["name"]!.ToString() },
							{"description", item["description"]!.ToString() },
							{"attribute", item["attribute"]! },
							{"type", item["type"]!.ToString() },
							{"value", item["value"]!.ToString() },
							{"helper", item["helper"]!.ToString() },
						};
						_dba.InsertCellPointer(dctCellPointer);
					}
				}
			}
		}

		public void UpdateValueChangedPointer(Cell cell, Pointer pointer, JProperty property)
		{
			_dba.UpdateCellPointerValue(cell.AppID, pointer.Pid, pointer.ToString());

			if (cell.MonitoringState == true)
			{
				if (PointerUpdated != null)
					PointerUpdated(new PointerUpdatedEventArgs(cell.AppID, pointer.Pid, property));
			}
			else
			{
				if (pointer.MonitoringState == true)
					if (PointerUpdated != null)
						PointerUpdated!(new PointerUpdatedEventArgs(cell.AppID, pointer.Pid, property));
			}
		}

		public void LoadCellType(string strCellType)
		{
			if (_dba.QueryCellTypeInfo(strCellType, out DataSet? dsCellInfo) == true)
			{
				Int64 cell_id = dsCellInfo!.Tables[0].Rows[0].Field<Int64>("cell_id");
				string strCode = dsCellInfo!.Tables[0].Rows[0].Field<string>("code")!;
				string strLibrary = dsCellInfo!.Tables[0].Rows[0].Field<string>("library")!;
				string code_or_library = dsCellInfo!.Tables[0].Rows[0].Field<string>("code_or_library")!;
				Type cell_type;

				cell_type = GetCellConstructObject(cell_id, strCellType, code_or_library, strCode, strLibrary);

				CellTypeInfo ci = new CellTypeInfo(dsCellInfo.Tables[0].Rows[0], cell_type);

				lock (_dctCellType2Cell)
				{
					lock (_dctCellID2Cell)
					{
						if (!_lstLockedCellIDs.Contains(cell_id))
						{
							_dctCellType2Cell[strCellType] = ci;
							_dctCellID2Cell[cell_id] = ci;
						}
						else
						{
							throw new CellTypeLockedException($"CellType {strCellType} is Locked.");
						}
					}
				}
			}
		}

		public bool IsCellTypeExist(string strCellType, string strCellUUID, out Int64 cell_id)
		{
			bool blRet = _dba.IsCellTypeExist(strCellType, strCellUUID, out cell_id);

			return blRet;
		}

		private Cell? LoadCellApp(Int64 cell_id, Int64 app_id)
		{
			//object[] objs = new object[] { (UInt32)app_id };
			if (Activator.CreateInstance(_dctCellID2Cell[cell_id].Constructor, new object[] { app_id }) is Cell app)
			{
				app.UUID = new Utils.LBUUID(app_id);
				app.CellID = cell_id;
				//ConstructorInfo[] ci = _dctCellID2Cell[cell_id].Constructor.GetConstructors();
				//Cell app = (Cell)ci[0].Invoke(new object[] { app_id });
				//app.UUID = new Utils.LBUUID(app_id);

				lock (_dctAppID2App)
				{
					_dctAppID2App[app_id] = app;
				}

				lock (_dctCellID2Apps)
				{
					if (!_dctCellID2Apps.TryGetValue(cell_id, out List<Cell>? lstApps))
					{
						lstApps = new List<Cell>();
						_dctCellID2Apps[cell_id] = lstApps;
					}

					lstApps.Add(app);
				}

				app.Initialize();

				return app;
			}
			else
			{
				return null;
			}
		}

		public bool RemoveCellType(Int64 cell_id, string cell_uuid)
		{
			bool blRet = true;

			lock (_dctCellID2Cell)
			{
				lock (_dctCellType2Cell)
				{
					lock (_dctCellID2Apps)
					{
						if (_dctCellID2Apps.TryGetValue(cell_id, out List<Cell>? apps) == true)
						{
							foreach (var app in apps)
							{
								StopApp(app);
								_dba.RemoveCellApp(app.AppID, out string? errMsg);
								lock (_dctAppID2App)
								{
									_dctAppID2App.Remove(app.AppID);
								}
							}
						}

						_dba.RemoveCellType(cell_id, cell_uuid, out string err_msg);
						_dctCellID2Apps.Remove(cell_id);

						CellTypeInfo ci = _dctCellID2Cell[cell_id];
						_dctCellID2Cell.Remove(cell_id);
						_dctCellType2Cell.Remove(ci.Type);

						blRet = true;
					}
				}
			}

			return blRet;
		}

		private void StopApp(Cell app)
		{
			app.Stop();
		}

		private void StopApp(Int64 app_id)
		{
			var app = _dctAppID2App[app_id];
			app.Stop();
		}

		/// <summary>
		/// jtPointers: {ptrs: [{appid:appid, pointers:[]}, ...], errbreak:true}
		/// </summary>
		/// <param name="jtPointers"></param>
		/// <param name="jtResp"></param>
		/// <returns></returns>
		public bool SetPointersValue(JToken jtPointers, JArray jaResp)
		{
			bool blRet = false;
			JObject joPointers = (jtPointers as JObject)!;
			JArray jaPtrs = (joPointers[Cell.PTRS] as JArray)!;

			if (joPointers.TryGetValue("errbreak", StringComparison.CurrentCulture, out JToken? jtErrBreak))

			foreach (JObject joAppPointers in jaPtrs)
			{
				if (joAppPointers.ContainsKey(Cell.APPID) && joAppPointers.ContainsKey(Cell.POINTERS))
				{
					Int64 appid = joAppPointers.Value<Int64>(Cell.APPID);
					JArray jaPointers = (joAppPointers[Cell.POINTERS] as JArray)!;

					blRet = _dctAppID2App[appid].SetPointersValue(jaPointers, jaResp, (bool)jtErrBreak);
				}
			}

			return blRet;
		}

		public bool CreateCellApps(Int64 cell_id, int nCount, bool blErrBreak, out DataTable? dtAppIDs, out string? errMsg)
		{
			return _dba.CreateCellApps(cell_id, nCount, blErrBreak, out dtAppIDs, out errMsg);
		}

		public bool RemoveCellApps(long[] app_ids, out JArray? jaResult)
		{
			bool blRet = false;

			jaResult = new JArray();
			foreach (long app_id in app_ids)
			{
				blRet = _dba.RemoveCellApp(app_id, out string? strErrMsg);
				if (blRet == false)
				{
					jaResult.Add(new JObject() { ["app_id"] = app_id, ["err_msg"] = strErrMsg });
				}
			}

			return jaResult.Count > 0;
		}

		private Type GetCellConstructObject(Int64 cell_id, string strCellType, string code_or_library, string strCode, string strLibrary)
		{
			string strAssemblyDir = Path.Combine(_strCellDllPath, strCellType);
			string strAssemblyFileName = "lib" + strCellType + ".dll";
			string strAssemblyFilePath = Path.Combine(strAssemblyDir, strAssemblyFileName);

			if (!Directory.Exists(strAssemblyDir))
			{
				Directory.CreateDirectory(strAssemblyDir);
			}

			if (!File.Exists(strAssemblyFilePath))
			{
				if (code_or_library == "code")
				{
					CompileCellCode(strCode, strAssemblyFilePath);
				}
				else if (code_or_library == "library")
				{
					SaveLibraryFile(strLibrary, strAssemblyFilePath);
				}
				else
				{
					throw new InvalidDataException($"Unknow code_or_library parameter: {code_or_library}");
				}
			}

			Assembly lib = Assembly.LoadFrom(strAssemblyFilePath);

			string strFullCellType = "LittleBee.Cells." + strCellType;
			foreach (Type t in lib.GetExportedTypes())
			{
				if (t.FullName == strFullCellType)
					return t;	// (Cell)Activator.CreateInstance(t);
			}

			throw new NotSupportedException($"Not supported cell type: {strCellType}");
		}

		public static int CompileCode(string strCode, CompilerParameters cp, out CompilerResults cr)
		{
			CSharpCodeProvider provider = new CSharpCodeProvider();

			cr = provider.CompileAssemblyFromSource(cp, strCode);

			return cr.Errors.Count;
		}

		private bool SaveLibraryFile(string strLibrary, string assemblyPath)
		{
			File.WriteAllBytes(assemblyPath, Utils.Utils.DecodeBase64(strLibrary));

			return true;
		}

		private bool CompileCellCode(string strCode, string assemblyPath)
		{
			bool blRet = false;
			CompilerParameters cp = new CompilerParameters()
			{
				GenerateExecutable = false,
				GenerateInMemory = false,
				OutputAssembly = assemblyPath
			};
			cp.ReferencedAssemblies.Add("System.dll");
			cp.ReferencedAssemblies.Add("System.Core.dll");
			cp.ReferencedAssemblies.Add("Newtonsoft.Json.dll");
			cp.ReferencedAssemblies.Add("libCell.dll");
			cp.ReferencedAssemblies.Add("libIDAL.dll");
			cp.ReferencedAssemblies.Add("libdba.dll");
			cp.ReferencedAssemblies.Add("libEngUnit.dll");
			cp.ReferencedAssemblies.Add("libUtils.dll");

			int errCount = CompileCode(strCode, cp, out CompilerResults cr);
			if (errCount == 0)
			{
				blRet = true;
			}
			else
			{
				StringBuilder sb = new StringBuilder();

				foreach (CompilerError ce in cr.Errors)
				{
					sb.Append(ce.ToString());
				}
				throw new CodeCompileErrorException(sb.ToString());
			}

			return blRet;
		}

		public bool GetCellSummaryInfo(out DataTable? dtCellSummary)
		{
			return _dba.GetCellSummaryInfo(out dtCellSummary);
		}

		public void Dispose()
		{
		}
	}
}
